-
Notifications
You must be signed in to change notification settings - Fork 231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(async-flow): add more context to vow rejection #10359
base: master
Are you sure you want to change the base?
Conversation
ffdb74c
to
cc5a1fb
Compare
Deploying agoric-sdk with
|
Latest commit: |
733042d
|
Status: | ✅ Deploy successful! |
Preview URL: | https://61d5d4cf.agoric-sdk.pages.dev |
Branch Preview URL: | https://mfig-10147-flow-rejects.agoric-sdk.pages.dev |
0740c55
to
fc1f17a
Compare
a66a9ce
to
49b4c9f
Compare
49b4c9f
to
588d227
Compare
packages/async-flow/src/template.js
Outdated
export const makeTemplateStringsArray = strings => | ||
harden(assign([...strings], { raw: strings })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clever, but the attributes matter. The raw
should be non-enumerable. How about
export const makeTemplateStringsArray = strings => | |
harden(assign([...strings], { raw: strings })); | |
export const makeTemplateStringsArray = strings => { | |
const result = [...strings]; | |
defineProperty(result, 'raw', { | |
value: strings, // Note: same rather than raw version | |
enumerable: false, // not needed, but good to be explicit | |
}); | |
return harden(result); | |
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I finessed your suggestion a little bit to avoid needing to coerce result
with a typecast.
@@ -22,6 +22,12 @@ export type FlowState = | |||
export type Guest<T extends unknown = any> = T; | |||
export type Host<T extends Passable = Passable> = T; | |||
|
|||
export type HostCall = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just from the name, I expected a HostCall
to also include the arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll explain why this is not the case in a comment (basically, the replay membrane doesn't need them to produce good-enough errors).
@@ -11,10 +11,21 @@ const BEST_GUESS_ID_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; | |||
* Return an object that mimics a template strings array. | |||
* | |||
* @param {string[]} strings | |||
* @param {string[]} [raw] optional raw strings, defaults to `strings` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
packages/async-flow/src/template.js
Outdated
// Make the raw property non-enumerable. | ||
defineProperty(result, 'raw', { | ||
value: raw, | ||
enumerable: false, // not needed, but good to be explicit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the "finessed" change, the explicit enumerable: false,
is needed.
enumerable: false, // not needed, but good to be explicit | |
enumerable: false, |
I have not reviewed yet, but I'm surprised we need a durable store for this. Since we always replay, why can't we use an ephemeral store? |
Ah, good catch. I'll try with an ephemeral store and ensure it still passes tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still need to parse the test for this, but here are my preliminary comments on the implementation.
if (typeof v === 'function') { | ||
return { | ||
[p](...args) { | ||
if (recording) recording.push({ thisArg: target, method: p, args }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't thisArg
a little misleading as a name if it records the proxy's target.
apply(target, thisArg, args) { | ||
if (recording) recording.push({ thisArg, func: target, args }); | ||
return Reflect.apply(target, thisArg, args); | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we care about this? Do we ever have a callable console?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: the tag
config additions are in commit before makeReplayMembrane
adds support / documentation for that config.
args, | ||
}); | ||
|
||
t.true(recording.every(entry => entry.thisArg === originalConsole)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think by construction of the recording console, this can only be true, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this file seem to be testing behavior introduced in a later commit.
initHostCall(vow, { | ||
target: hostTarget, | ||
method: optVerb, | ||
eventual: true, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah with eventual send it's gonna be more difficult to record on the guest side since we don't see the guest anymore.
); | ||
if (hostVowToCall) { | ||
const hostVowCap = toPassableCap(hostVow); | ||
if (hostVowToCall.has(hostVowCap)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what case would the map not have the cap?
if (hostVowToCall) { | ||
const hostVowCap = toPassableCap(hostVow); | ||
if (hostVowToCall.has(hostVowCap)) { | ||
const hostCall = hostVowToCall.get(hostVowCap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason to leave the host call in the map after rejection? I think doFulfill should remove from the map as well.
@@ -118,6 +118,7 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => { | |||
bijection, // membrane's guest-host mapping | |||
outcomeKit: makeVowKit(), // outcome of activation as host vow | |||
isDone: false, // persistently done | |||
hostVowToCall: zone.detached().weakMapStore('hostVowToCall'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Besides changing to an ephemeral store, I think we shouldn't burden ourselves with weak collections and their gc costs since we have delimited lifetimes for these host call entries, and don't need to retain them past settlement. Since the replay membrane is keeping the result vow/promises live anyway, there is no reason to assume those might be dropped earlier either.
* @param {any[]} args | ||
* @returns {[TemplateStringsArray, ...any[]]} | ||
*/ | ||
export const inlineTemplateArgs = (tmpl, ...args) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering if the ergonomics wouldn't be simpler with a wrapper for the original template tag function:
inlineArgs(X)`foo bar ${inlinedArg}`;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was a little hard to digest the test. Wondering how we can simplify its reading.
const consStack = i => { | ||
const re = indexToStackRegex[i]; | ||
delete indexToStackRegex[i]; | ||
t.truthy(re, `recorded entry ${i} has a stack regex`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not accept the re
as an argument instead of this definition indirection?
const consStack = i => { | |
const re = indexToStackRegex[i]; | |
delete indexToStackRegex[i]; | |
t.truthy(re, `recorded entry ${i} has a stack regex`); | |
const consStack = (i, re) => { | |
t.truthy(re, `recorded entry ${i} has a stack regex`); |
const error = (...args) => cons('error', ...args); | ||
const group = (...args) => cons('group', ...args); | ||
|
||
const expectedRecording = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little weird to see assertions being made during the declaration of the expected value, but then I'm not sure how to do it any better. I also think that it'd be better to run the main "deepEqual" assertion before any nested assertions for specific entries.
I'm wondering if something like the helpers returning a tuple of "method + args" and an optional thunk to run extra assertions. The "method +args" would be used to build an expected result that can be used first with deepEqual
. The thunks if present would then be invoked one by one on each recorded entry. The thunk could have a signature of (recordedArgs, i)
so that the helper definitions don't need to close over the recording
input.
test.serial('test host vow rejection logging', async t => { | ||
annihilate(); | ||
const zone1 = makeDurableZone(getBaggage(), 'durableRoot'); | ||
await testRejectionPlay(t, zone1); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'd like to see this test also written with a restart where the vow is only resolved in the new incarnation.
async guestMethod(gOrch7) { | ||
const p2 = gOrch7.vow(); | ||
|
||
await eventLoopIteration(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need this new crank?
setImmediate(() => { | ||
gOrch7.reject(Error('should be seen')); | ||
}); | ||
|
||
await eventLoopIteration(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while semantically equivalent to an await eventLoopIteration()
followed by a call to reject
, I assume this is written like this to put the rejection on an empty stack even if the engine has async stack traces? Can we add a comment to this effect?
|
||
await eventLoopIteration(); | ||
|
||
const caughtP = p2.catch(e => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to sanity check that the promise doesn't fulfill.
const caughtP = p2.catch(e => { | |
const caughtP = p2.then( | |
v => t.fail(`vow fulfilled with value ${v}`), | |
e => { |
3: error('Error#1 ERROR_NOTE:', 'from host error', '(Error#2)'), | ||
4: error( | ||
'Error#1 ERROR_NOTE:', | ||
'from flow', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it read better as?
'from flow', | |
'during execution of flow', |
[recLen17 - 6]: error('Error#4:', 'should be seen'), | ||
[recLen17 - 5]: consStack(recLen17 - 5), | ||
[recLen17 - 4]: cons('groupEnd'), | ||
[recLen17 - 3]: error('Error#3:', 'this stack'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to have a message a little more descriptive? maybe:
[recLen17 - 3]: error('Error#3:', 'this stack'), | |
[recLen17 - 3]: error('Error#3:', 'AsyncFlow1 definition site'), |
closes: #10147
Description
When an asyncFlow guest calls the host through its replay membrane, keep an ephemeral weakMapStore from any result hostVow to the target, method, (and eventually, whether it is an "eventual" result).
If that hostVow is rejected with an error, annotate the error as described in #10147.
Security Considerations
Error annotations are only revealed to the base causal console, which cannot be captured in vat code (unlike the consoleRecorder shim that is set up during tests in this PR).
Scaling Considerations
No significant increase yet.
Documentation Considerations
n/a
Testing Considerations
n/a
Upgrade Considerations
n/a